From a3f6a404be484154ece005084599933517767fcb Mon Sep 17 00:00:00 2001 From: xanxys Date: Sat, 9 Aug 2014 14:37:50 +0900 Subject: [PATCH] Add "--list" option to `cargo`, that shows lists of installed (sub)commands by searching directories for executables with name cargo-*. --- src/bin/cargo.rs | 120 ++++++++++++++++++++++++++++++++++++-------- tests/test_cargo.rs | 69 +++++++++++++++++++++++++ tests/tests.rs | 1 + 3 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 tests/test_cargo.rs diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index 828cfd91c..ac2a3e06f 100644 --- a/src/bin/cargo.rs +++ b/src/bin/cargo.rs @@ -7,7 +7,10 @@ extern crate cargo; extern crate docopt; #[phase(plugin)] extern crate docopt_macros; +use std::collections::TreeSet; use std::os; +use std::io; +use std::io::fs; use std::io::process::{Command,InheritFd,ExitStatus,ExitSignal}; use serialize::Encodable; use docopt::FlagParser; @@ -28,10 +31,12 @@ Usage: cargo [...] cargo -h | --help cargo -V | --version + cargo --list Options: -h, --help Display this message -V, --version Print version info and exit + --list List installed commands -v, --verbose Use verbose output Some common cargo commands are: @@ -54,6 +59,14 @@ See 'cargo help ' for more information on a specific command. fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult> { debug!("executing; cmd=cargo; args={}", os::args()); shell.set_verbose(flags.flag_verbose); + if flags.flag_list { + println!("Installed Commands:"); + for command in list_commands().iter() { + println!(" {}", command); + // TODO: it might be helpful to add result of -h to each command. + }; + return Ok(None) + } let mut args = flags.arg_args.clone(); args.insert(0, flags.arg_command.clone()); match flags.arg_command.as_slice() { @@ -82,29 +95,25 @@ fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult> { let r = cargo::call_main_without_stdin(execute, shell, ["-h".to_string()], false); cargo::process_executed(r, shell) - } + }, orig_cmd => { - let cmd = if orig_cmd == "help" { + let is_help = orig_cmd == "help"; + let cmd = if is_help { flags.arg_args[0].as_slice() } else { orig_cmd }; - let command = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX); - let mut command = match os::self_exe_path() { - Some(path) => { - let p1 = path.join("../lib/cargo").join(command.as_slice()); - let p2 = path.join(command.as_slice()); - if p1.exists() { - Command::new(p1) - } else if p2.exists() { - Command::new(p2) - } else { - Command::new(command) - } - } - None => Command::new(command), - }; - let command = if orig_cmd == "help" { + execute_subcommand(cmd, is_help, &flags, shell) + } + } + Ok(None) +} + +fn execute_subcommand(cmd: &str, is_help: bool, flags: &Flags, shell: &mut MultiShell) -> () { + match find_command(cmd) { + Some(command) => { + let mut command = Command::new(command); + let command = if is_help { command.arg("-h") } else { command.args(flags.arg_args.as_slice()) @@ -124,12 +133,81 @@ fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult> { let msg = format!("subcommand failed with signal: {}", i); handle_error(CliError::new(msg, i as uint), shell) } - Err(_) => handle_error(CliError::new("No such subcommand", 127), - shell) + Err(io::IoError{kind, ..}) if kind == io::FileNotFound => + handle_error(CliError::new("No such subcommand", 127), shell), + Err(err) => handle_error( + CliError::new( + format!("Subcommand failed to run: {}", err), 127), + shell) + } + }, + None => handle_error(CliError::new("No such subcommand", 127), shell) + } +} + +/// List all runnable commands. find_command should always succeed +/// if given one of returned command. +fn list_commands() -> TreeSet { + let command_prefix = "cargo-"; + let mut commands = TreeSet::new(); + for dir in list_command_directory().iter() { + let entries = match fs::readdir(dir) { + Ok(entries) => entries, + _ => continue + }; + for entry in entries.iter() { + let filename = match entry.filename_str() { + Some(filename) => filename, + _ => continue + }; + if filename.starts_with(command_prefix) && + filename.ends_with(os::consts::EXE_SUFFIX) && + is_executable(entry) { + let command = filename.slice( + command_prefix.len(), + filename.len() - os::consts::EXE_SUFFIX.len()); + commands.insert(String::from_str(command)); } } } - Ok(None) + commands +} + +fn is_executable(path: &Path) -> bool { + match fs::stat(path) { + Ok(io::FileStat{kind, perm, ..}) => + (kind == io::TypeFile) && perm.contains(io::OtherExecute), + _ => false + } +} + +/// Get `Command` to run given command. +fn find_command(cmd: &str) -> Option { + let command_exe = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX); + let dirs = list_command_directory(); + let mut command_paths = dirs.iter().map(|dir| dir.join(command_exe.as_slice())); + command_paths.find(|path| path.exists()) +} + +/// List candidate locations where subcommands might be installed. +fn list_command_directory() -> Vec { + let mut dirs = vec![]; + match os::self_exe_path() { + Some(path) => { + dirs.push(path.join("../lib/cargo")); + dirs.push(path); + }, + None => {} + }; + match std::os::getenv("PATH") { + Some(val) => { + for dir in os::split_paths(val).iter() { + dirs.push(Path::new(dir)) + } + }, + None => {} + }; + dirs } #[deriving(Encodable)] diff --git a/tests/test_cargo.rs b/tests/test_cargo.rs new file mode 100644 index 000000000..bf32edaa8 --- /dev/null +++ b/tests/test_cargo.rs @@ -0,0 +1,69 @@ +use cargo::util::{process, ProcessBuilder}; +use hamcrest::{assert_that}; +use std::io; +use std::io::fs; +use std::os; +use support::paths; +use support::{project, execs, cargo_dir, mkdir_recursive, ProjectBuilder, ResultTest}; + +fn setup() { +} + +/// Add an empty file with executable flags (and platform-dependent suffix). +/// TODO: move this to `ProjectBuilder` if other cases using this emerge. +fn fake_executable(proj: ProjectBuilder, dir: &Path, name: &str) -> ProjectBuilder { + let path = proj.root().join(dir).join(format!("{}{}", name, os::consts::EXE_SUFFIX)); + mkdir_recursive(&Path::new(path.dirname())).assert(); + fs::File::create(&path).assert(); + let io::FileStat{perm, ..} = fs::stat(&path).assert(); + fs::chmod(&path, io::OtherExecute | perm).assert(); + proj +} + +/// Copy real cargo exeutable just built to specified location, and +/// prepare to run it. +fn copied_executable_process(proj: &ProjectBuilder, name: &str, dir: &Path) -> ProcessBuilder { + let name = format!("{}{}", name, os::consts::EXE_SUFFIX); + let path_src = cargo_dir().join(name.clone()); + let path_dst = proj.root().join(dir).join(name); + mkdir_recursive(&Path::new(path_dst.dirname())).assert(); + fs::copy(&path_src, &path_dst).assert(); + process(path_dst) + .cwd(proj.root()) + .env("HOME", Some(paths::home().as_vec())) +} + +test!(list_commands_empty { + let proj = project("list-runs"); + let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list"); + assert_that(pr, execs() + .with_status(0) + .with_stdout("Installed Commands:\n")); +}) + +test!(list_commands_non_overlapping { + // lib/cargo | cargo-3 + // bin/ | cargo-2 + // PATH | cargo-1 + // Check if --list searches all 3 targets. + // Also checks that results are in lexicographic order. + let proj = project("list-non-overlapping"); + let proj = fake_executable(proj, &Path::new("lib/cargo"), "cargo-3"); + let proj = fake_executable(proj, &Path::new("bin"), "cargo-2"); + let proj = fake_executable(proj, &Path::new("path-test"), "cargo-1"); + let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list"); + + let path_test = proj.root().join("path-test"); + // On Windows, cargo.exe seems to require some directory ( + // I don't know which) to run properly. + // That's why we append to $PATH here, instead of overwriting. + let path = os::getenv_as_bytes("PATH").unwrap(); + let mut components = os::split_paths(path); + components.push(path_test); + let path_var = os::join_paths(components.as_slice()).assert(); + assert_that( + pr.env("PATH", Some(path_var.as_slice())), + execs() + .with_status(0) + .with_stdout("Installed Commands:\n 1\n 2\n 3\n")); +}) diff --git a/tests/tests.rs b/tests/tests.rs index 3672cd33a..73ca0dea6 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -20,6 +20,7 @@ macro_rules! test( ) ) +mod test_cargo; mod test_cargo_clean; mod test_cargo_compile; mod test_cargo_compile_git_deps; -- 2.30.2